package redis

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"time"

	"github.com/go-redis/redis/v8"
)

var (
	ErrLockFailed      = errors.New("failed to acquire lock")
	ErrLockNotHeld     = errors.New("lock not held")
	ErrInvalidDuration = errors.New("block duration cannot exceed lock TTL")
)

// RedisLock 分布式锁结构体
type RedisLock struct {
	client     *redis.Client
	key        string
	ttl        time.Duration
	retryDelay time.Duration
}

// New 创建新的RedisLock实例
func New(client *redis.Client, key string, ttl time.Duration) *RedisLock {
	return &RedisLock{
		client:     client,
		key:        key,
		ttl:        ttl,
		retryDelay: 100 * time.Millisecond,
	}
}

// WithRetryDelay 设置重试间隔
func (l *RedisLock) WithRetryDelay(delay time.Duration) *RedisLock {
	l.retryDelay = delay
	return l
}

// Get 获取锁并执行回调函数
func (l *RedisLock) Get(block time.Duration, callback func()) error {
	if block > l.ttl {
		return ErrInvalidDuration
	}
	ctx := context.Background()
	token := generateSecureToken()
	deadline := time.Now().Add(block)
	for {
		acquired, err := l.acquire(ctx, l.key, token)
		if err != nil {
			return err
		}
		if acquired {
			defer l.release(ctx, l.key, token)
			callback()
			return nil
		}
		if block == 0 || time.Now().After(deadline) {
			return ErrLockFailed
		}
		time.Sleep(l.retryDelay)
	}
}

// 申请锁
func (l *RedisLock) acquire(ctx context.Context, key string, token string) (bool, error) {
	// 使用SET NX命令，并设置过期时间
	result, err := l.client.SetNX(ctx, key, token, l.ttl).Result()
	if err != nil {
		return false, fmt.Errorf("%w: %v", ErrLockFailed, err)
	}
	return result, nil
}

// 释放锁
func (l *RedisLock) release(ctx context.Context, key string, token string) error {
	script := `
	if redis.call("get", KEYS[1]) == ARGV[1] then
		return redis.call("del", KEYS[1])
	else
		return 0
	end
	`
	result, err := l.client.Eval(ctx, script, []string{key}, token).Int()
	if err != nil {
		return err
	}
	if result == 0 {
		return ErrLockNotHeld
	}
	return nil
}

// 生成安全的随机token
func generateSecureToken() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	return base64.URLEncoding.EncodeToString(b)
}
